import subprocess
import os
import shutil
from typing import Tuple
from abc import ABC, abstractmethod

class PackageManagerStrategy(ABC):
    """包管理器策略抽象基类"""
    
    @abstractmethod
    def install_dependencies(self, npm_manager) -> Tuple[bool, str]:
        pass
    
    @abstractmethod
    def get_build_output_path(self, project_path) -> Tuple[bool, str]:
        pass
    
    @abstractmethod
    def get_command(self) -> str:
        pass


class NpmStrategy(PackageManagerStrategy):
    """NPM策略实现"""
    
    def install_dependencies(self, npm_manager) -> Tuple[bool, str]:
        """使用npm安装依赖"""
        try:
            # 使用npm安装依赖
            command = [npm_manager.NPM_COMMAND, "install", "--platform=neutral", "--no-optional=false", "--force"]
            print(f"使用npm安装依赖: {' '.join(command)}")
            
            # 在Windows上指定编码以避免UnicodeDecodeError
            if os.name == 'nt':  # Windows
                result = subprocess.run(
                    command,
                    cwd=npm_manager.project_path,
                    capture_output=True,
                    text=True,
                    encoding='utf-8',
                    errors='ignore',
                    timeout=300,  # 5分钟超时
                    env=npm_manager.env
                )
            else:
                result = subprocess.run(
                    command,
                    cwd=npm_manager.project_path,
                    capture_output=True,
                    text=True,
                    timeout=300,  # 5分钟超时
                    env=npm_manager.env
                )
            
            print(f"安装命令返回码: {result.returncode}")
            if result.returncode != 0:
                stdout_str = result.stdout or ""
                stderr_str = result.stderr or ""
                print(f"标准输出: {stdout_str}")
                print(f"错误输出: {stderr_str}")
                # 如果是平台相关错误，尝试使用更严格的参数
                if stderr_str and "EBADPLATFORM" in stderr_str:
                    print("检测到平台兼容性问题，尝试使用更严格的参数")
                    strict_command = command + ["--ignore-platform"]
                    print(f"使用严格参数重新尝试: {' '.join(strict_command)}")
                    # 在Windows上指定编码以避免UnicodeDecodeError
                    if os.name == 'nt':  # Windows
                        result = subprocess.run(
                            strict_command,
                            cwd=npm_manager.project_path,
                            capture_output=True,
                            text=True,
                            encoding='utf-8',
                            errors='ignore',
                            timeout=300,
                            env=npm_manager.env
                        )
                    else:
                        result = subprocess.run(
                            strict_command,
                            cwd=npm_manager.project_path,
                            capture_output=True,
                            text=True,
                            timeout=300,
                            env=npm_manager.env
                        )
                    if result.returncode != 0:
                        stdout_str2 = result.stdout or ""
                        stderr_str2 = result.stderr or ""
                        print(f"严格参数下仍然失败，标准输出: {stdout_str2}")
                        print(f"严格参数下仍然失败，错误输出: {stderr_str2}")
                        return False, f"依赖安装失败: {stderr_str2}"
            else:
                # 即使成功，如果有错误输出也打印出来供调试
                stderr_str = result.stderr or ""
                if stderr_str:
                    print(f"警告信息: {stderr_str}")
            
            return True, "依赖安装成功"
        except subprocess.TimeoutExpired:
            return False, "依赖安装超时"
        except Exception as e:
            return False, f"安装依赖时发生错误: {str(e)}"
    
    def get_build_output_path(self, project_path) -> Tuple[bool, str]:
        """获取npm构建输出路径"""
        build_output_path = os.path.join(project_path, "dist")
        if not os.path.exists(build_output_path):
            return False, f"npm构建输出目录不存在: {build_output_path}"
        return True, build_output_path
    
    def get_command(self) -> str:
        """获取命令名称"""
        return "npm"


class PnpmStrategy(PackageManagerStrategy):
    """PNPM策略实现"""
    
    def install_dependencies(self, npm_manager) -> Tuple[bool, str]:
        """使用pnpm安装依赖"""
        try:
            # 使用pnpm安装依赖
            command = [npm_manager.PNPM_COMMAND, "install", "--config.platform=neutral", "--force"]
            print(f"使用pnpm安装依赖: {' '.join(command)}")
            
            # 在Windows上指定编码以避免UnicodeDecodeError
            if os.name == 'nt':  # Windows
                result = subprocess.run(
                    command,
                    cwd=npm_manager.project_path,
                    capture_output=True,
                    text=True,
                    encoding='utf-8',
                    errors='ignore',
                    timeout=300,  # 5分钟超时
                    env=npm_manager.env
                )
            else:
                result = subprocess.run(
                    command,
                    cwd=npm_manager.project_path,
                    capture_output=True,
                    text=True,
                    timeout=300,  # 5分钟超时
                    env=npm_manager.env
                )
            
            print(f"安装命令返回码: {result.returncode}")
            if result.returncode != 0:
                stdout_str = result.stdout or ""
                stderr_str = result.stderr or ""
                print(f"标准输出: {stdout_str}")
                print(f"错误输出: {stderr_str}")
                # 如果是平台相关错误，尝试使用更严格的参数
                if stderr_str and "EBADPLATFORM" in stderr_str:
                    print("检测到平台兼容性问题，尝试使用更严格的参数")
                    strict_command = command + ["--ignore-platform"]
                    print(f"使用严格参数重新尝试: {' '.join(strict_command)}")
                    # 在Windows上指定编码以避免UnicodeDecodeError
                    if os.name == 'nt':  # Windows
                        result = subprocess.run(
                            strict_command,
                            cwd=npm_manager.project_path,
                            capture_output=True,
                            text=True,
                            encoding='utf-8',
                            errors='ignore',
                            timeout=300,
                            env=npm_manager.env
                        )
                    else:
                        result = subprocess.run(
                            strict_command,
                            cwd=npm_manager.project_path,
                            capture_output=True,
                            text=True,
                            timeout=300,
                            env=npm_manager.env
                        )
                    if result.returncode != 0:
                        stdout_str2 = result.stdout or ""
                        stderr_str2 = result.stderr or ""
                        print(f"严格参数下仍然失败，标准输出: {stdout_str2}")
                        print(f"严格参数下仍然失败，错误输出: {stderr_str2}")
                        return False, f"依赖安装失败: {stderr_str2}"
            else:
                # 即使成功，如果有错误输出也打印出来供调试
                stderr_str = result.stderr or ""
                if stderr_str:
                    print(f"警告信息: {stderr_str}")
            
            return True, "依赖安装成功"
        except subprocess.TimeoutExpired:
            return False, "依赖安装超时"
        except Exception as e:
            return False, f"安装依赖时发生错误: {str(e)}"
    
    def get_build_output_path(self, project_path) -> Tuple[bool, str]:
        """获取pnpm构建输出路径"""
        # 使用pnpm时，检查 dist/build/h5 目录
        build_output_path = os.path.join(project_path, "dist", "build", "h5")
        if not os.path.exists(build_output_path):
            # 如果 dist/build/h5 不存在，检查 dist/h5 目录
            build_output_path = os.path.join(project_path, "dist", "h5")
            if not os.path.exists(build_output_path):
                return False, f"pnpm构建输出目录不存在: {build_output_path}"
        return True, build_output_path
    
    def get_command(self) -> str:
        """获取命令名称"""
        return "pnpm"


class NpmManager:
    """NPM包管理及构建工具类"""
    
    # 可配置的NPM命令，根据操作系统选择默认值
    if os.name == 'nt':  # Windows
        NPM_COMMAND = os.getenv('NPM_COMMAND', 'npm.cmd')
        PNPM_COMMAND = os.getenv('PNPM_COMMAND', 'pnpm.cmd')
    else:  # Linux/Mac
        NPM_COMMAND = os.getenv('NPM_COMMAND', 'npm')
        PNPM_COMMAND = os.getenv('PNPM_COMMAND', 'pnpm')
    
    # 确保命令变量不为None
    if not NPM_COMMAND:
        NPM_COMMAND = 'npm' if os.name != 'nt' else 'npm.cmd'
    if not PNPM_COMMAND:
        PNPM_COMMAND = 'pnpm' if os.name != 'nt' else 'pnpm.cmd'
    
    NODE_COMMAND = os.getenv('NODE_COMMAND', 'node')
    
    def __init__(self, project_path: str):
        self.project_path = project_path
        # 设置环境变量确保npm可以正常执行
        self.env = os.environ.copy()
        # 根据操作系统设置PATH
        if os.name != 'nt':  # Linux/Mac
            path_value = self.env.get('PATH', '')
            self.env['PATH'] = '/usr/local/bin:/usr/bin:/bin:' + path_value if path_value else '/usr/local/bin:/usr/bin:/bin'
        else:  # Windows
            # Windows上保持原有PATH
            pass
        
        # 检测是否应该使用pnpm（基于项目配置，不考虑pnpm是否安装）
        self.use_pnpm = self._should_use_pnpm()
        print(f"项目 {project_path} 是否应该使用 pnpm: {self.use_pnpm}")
        
        # 即使pnpm不可用，也应该检测项目是否需要使用pnpm
        if not self.use_pnpm:
            pnpm_lock_path = os.path.join(self.project_path, "pnpm-lock.yaml")
            if os.path.exists(pnpm_lock_path):
                print("手动检测到 pnpm-lock.yaml 文件，项目应该使用 pnpm")
                self.use_pnpm = True
        
        # 根据检测结果选择策略
        if self.use_pnpm:
            self.strategy = PnpmStrategy()
        else:
            self.strategy = NpmStrategy()
    
    def check_environment(self) -> Tuple[bool, str]:
        """检查Node.js和npm环境"""
        try:
            # 在Windows上指定编码以避免UnicodeDecodeError
            if os.name == 'nt':  # Windows
                node_check = subprocess.run(
                    [self.NODE_COMMAND, "--version"], 
                    capture_output=True, 
                    text=True, 
                    encoding='utf-8',
                    errors='ignore',
                    timeout=10, 
                    env=self.env
                )
                npm_check = subprocess.run(
                    [self.NPM_COMMAND, "--version"], 
                    capture_output=True, 
                    text=True, 
                    encoding='utf-8',
                    errors='ignore',
                    timeout=10, 
                    env=self.env
                )
            else:
                node_check = subprocess.run(
                    [self.NODE_COMMAND, "--version"], 
                    capture_output=True, 
                    text=True, 
                    timeout=10, 
                    env=self.env
                )
                npm_check = subprocess.run(
                    [self.NPM_COMMAND, "--version"], 
                    capture_output=True, 
                    text=True, 
                    timeout=10, 
                    env=self.env
                )
            
            node_version = node_check.stdout.strip() if node_check.returncode == 0 else '未找到'
            npm_version = npm_check.stdout.strip() if npm_check.returncode == 0 else '未找到'
            
            if node_check.returncode != 0 or npm_check.returncode != 0:
                return False, f"Node版本: {node_version}, NPM版本: {npm_version} - Node.js或npm未正确安装"
            
            # 如果使用pnpm，也检查pnpm版本
            pnpm_version = '未检查'
            if self.use_pnpm:
                try:
                    # 在Windows上指定编码以避免UnicodeDecodeError
                    if os.name == 'nt':  # Windows
                        pnpm_check = subprocess.run(
                            [self.PNPM_COMMAND, "--version"], 
                            capture_output=True, 
                            text=True, 
                            encoding='utf-8',
                            errors='ignore',
                            timeout=10, 
                            env=self.env
                        )
                    else:
                        pnpm_check = subprocess.run(
                            [self.PNPM_COMMAND, "--version"], 
                            capture_output=True, 
                            text=True, 
                            timeout=10, 
                            env=self.env
                        )
                    pnpm_version = pnpm_check.stdout.strip() if pnpm_check.returncode == 0 else '未找到'
                except Exception:
                    pnpm_version = '检查失败'
            
            return True, f"Node版本: {node_version}, NPM版本: {npm_version}, PNPM版本: {pnpm_version}"
        except subprocess.TimeoutExpired:
            return False, "检查Node.js环境超时"
        except FileNotFoundError:
            return False, "Node.js或npm未安装"
        except Exception as e:
            return False, f"检查Node.js环境时发生错误: {str(e)}"
    
    def _should_use_pnpm(self) -> bool:
        """检测是否应该使用pnpm"""
        # 直接检查是否有pnpm-lock.yaml文件，这是最可靠的判断方式
        pnpm_lock_path = os.path.join(self.project_path, "pnpm-lock.yaml")
        if os.path.exists(pnpm_lock_path):
            print("检测到 pnpm-lock.yaml 文件，应该使用 pnpm")
            return True
        
        # 检查package.json中是否有preinstall脚本要求使用pnpm
        package_json_path = os.path.join(self.project_path, "package.json")
        if os.path.exists(package_json_path):
            try:
                import json
                with open(package_json_path, 'r', encoding='utf-8') as f:
                    package_json = json.load(f)
                scripts = package_json.get("scripts", {})
                if "preinstall" in scripts and "only-allow pnpm" in scripts["preinstall"]:
                    print("检测到 package.json 中配置了 only-allow pnpm，应该使用 pnpm")
                    return True
            except Exception as e:
                print(f"读取 package.json 时出错: {e}")
                pass
        
        print("未检测到需要使用 pnpm 的条件，将使用 npm")
        return False
    
    def _is_command_available(self, command: str) -> bool:
        """检查命令是否可用"""
        if not command:
            return False
        
        try:
            # 在Windows上指定编码以避免UnicodeDecodeError
            if os.name == 'nt':  # Windows
                result = subprocess.run(
                    [command, "--version"],
                    capture_output=True,
                    text=True,
                    encoding='utf-8',
                    errors='ignore',
                    timeout=10,
                    env=self.env
                )
            else:
                result = subprocess.run(
                    [command, "--version"],
                    capture_output=True,
                    text=True,
                    timeout=10,
                    env=self.env
                )
            return result.returncode == 0
        except Exception as e:
            print(f"检查命令 {command} 是否可用时出错: {e}")
            return False
    
    def _clean_node_modules(self):
        """清理node_modules目录和lock文件"""
        import shutil
        import time
        try:
            # 删除node_modules目录
            node_modules_path = os.path.join(self.project_path, "node_modules")
            if os.path.exists(node_modules_path):
                print("删除node_modules目录...")
                # 在Windows上，有时需要一点时间来释放文件锁
                for attempt in range(5):  # 增加重试次数
                    try:
                        shutil.rmtree(node_modules_path, ignore_errors=False)
                        break
                    except Exception as e:
                        if attempt < 4:  # 增加重试次数
                            print(f"删除node_modules时遇到问题，{1 << attempt}秒后重试: {e}")
                            time.sleep(1 << attempt)  # 指数退避
                        else:
                            print(f"无法完全删除node_modules: {e}")
                            shutil.rmtree(node_modules_path, ignore_errors=True)
            
            # 删除package-lock.json (npm)
            package_lock_path = os.path.join(self.project_path, "package-lock.json")
            if os.path.exists(package_lock_path):
                print("删除package-lock.json文件...")
                try:
                    os.remove(package_lock_path)
                except Exception as e:
                    print(f"无法删除package-lock.json: {e}")
            
            # 删除pnpm-lock.yaml (pnpm)
            pnpm_lock_path = os.path.join(self.project_path, "pnpm-lock.yaml")
            if os.path.exists(pnpm_lock_path):
                print("删除pnpm-lock.yaml文件...")
                try:
                    os.remove(pnpm_lock_path)
                except Exception as e:
                    print(f"无法删除pnpm-lock.yaml: {e}")
                
            # 删除yarn.lock (yarn)
            yarn_lock_path = os.path.join(self.project_path, "yarn.lock")
            if os.path.exists(yarn_lock_path):
                print("删除yarn.lock文件...")
                try:
                    os.remove(yarn_lock_path)
                except Exception as e:
                    print(f"无法删除yarn.lock: {e}")
                
            # 删除.pnpm目录 (pnpm cache)
            pnpm_cache_path = os.path.join(self.project_path, ".pnpm")
            if os.path.exists(pnpm_cache_path):
                print("删除.pnpm目录...")
                for attempt in range(5):
                    try:
                        shutil.rmtree(pnpm_cache_path, ignore_errors=False)
                        break
                    except Exception as e:
                        if attempt < 4:
                            print(f"删除.pnpm时遇到问题，{1 << attempt}秒后重试: {e}")
                            time.sleep(1 << attempt)
                        else:
                            print(f"无法完全删除.pnpm: {e}")
                            shutil.rmtree(pnpm_cache_path, ignore_errors=True)
                
            # 删除dist目录
            dist_path = os.path.join(self.project_path, "dist")
            if os.path.exists(dist_path):
                print("删除dist目录...")
                # 使用带重试机制的删除方法
                for attempt in range(5):
                    try:
                        shutil.rmtree(dist_path, ignore_errors=False)
                        break
                    except Exception as e:
                        if attempt < 4:
                            print(f"删除dist时遇到问题，{1 << attempt}秒后重试: {e}")
                            time.sleep(1 << attempt)
                        else:
                            print(f"无法完全删除dist: {e}")
                            shutil.rmtree(dist_path, ignore_errors=True)
                
            print("清理完成")
        except Exception as e:
            print(f"清理过程中出错: {e}")
    
    def install_dependencies(self) -> Tuple[bool, str]:
        """安装项目依赖"""
        # 检查包管理器是否可用
        if self.use_pnpm:
            pnpm_available = self._is_command_available(self.PNPM_COMMAND)
            if not pnpm_available:
                print("项目需要使用pnpm，但pnpm不可用，回退到npm")
                self.strategy = NpmStrategy()
        else:
            npm_available = self._is_command_available(self.NPM_COMMAND)
            if not npm_available:
                # 如果npm不可用，检查是否可以使用pnpm
                pnpm_available = self._is_command_available(self.PNPM_COMMAND)
                if pnpm_available:
                    print("npm不可用，但pnpm可用，切换到pnpm")
                    self.strategy = PnpmStrategy()
                else:
                    return False, "没有可用的包管理器 (npm或pnpm)"
        
        # 使用策略执行依赖安装
        success, message = self.strategy.install_dependencies(self)
        
        # 如果安装失败，尝试再次安装（最多2次重试）
        if not success:
            for attempt in range(2):
                print(f"依赖安装失败，第{attempt + 1}次重试...")
                # 清理环境
                self._clean_node_modules()
                # 再次尝试安装
                success, message = self.strategy.install_dependencies(self)
                if success:
                    break
                else:
                    print(f"第{attempt + 1}次重试失败: {message}")
        
        return success, message
    
    def build_project(self) -> Tuple[bool, str]:
        """构建项目"""
        try:
            # 在Windows上指定编码以避免UnicodeDecodeError
            if os.name == 'nt':  # Windows
                result = subprocess.run(
                    [self.NPM_COMMAND, "run", "build"],
                    cwd=self.project_path,
                    capture_output=True,
                    text=True,
                    encoding='utf-8',
                    errors='ignore',
                    timeout=600,  # 10分钟超时
                    env=self.env
                )
            else:
                result = subprocess.run(
                    [self.NPM_COMMAND, "run", "build"],
                    cwd=self.project_path,
                    capture_output=True,
                    text=True,
                    timeout=600,  # 10分钟超时
                    env=self.env
                )
            
            if result.returncode != 0:
                stderr_str = result.stderr or ""
                # 检查是否是npm的可选依赖相关错误
                if "optional dependencies" in stderr_str or "Cannot find module" in stderr_str or "ELIFECYCLE" in stderr_str or "ERR_MODULE_NOT_FOUND" in stderr_str or "failed to load config" in stderr_str:
                    print("检测到依赖相关错误，尝试清理并重新构建...")
                    # 清理node_modules和lock文件
                    self._clean_node_modules()
                    # 重新安装依赖
                    install_success, install_msg = self.install_dependencies()
                    if install_success:
                        # 重新构建，最多尝试3次
                        for attempt in range(3):
                            print(f"重新安装依赖成功，第{attempt + 1}次尝试构建...")
                            if os.name == 'nt':  # Windows
                                result = subprocess.run(
                                    [self.NPM_COMMAND, "run", "build"],
                                    cwd=self.project_path,
                                    capture_output=True,
                                    text=True,
                                    encoding='utf-8',
                                    errors='ignore',
                                    timeout=600,
                                    env=self.env
                                )
                            else:
                                result = subprocess.run(
                                    [self.NPM_COMMAND, "run", "build"],
                                    cwd=self.project_path,
                                    capture_output=True,
                                    text=True,
                                    timeout=600,
                                    env=self.env
                                )
                            if result.returncode == 0:
                                return True, "项目重新构建成功"
                            else:
                                stderr_str = result.stderr or ""
                                print(f"第{attempt + 1}次构建失败: {stderr_str}")
                                # 如果不是最后一次尝试，清理并重新安装依赖
                                if attempt < 2:
                                    print("再次清理并重新安装依赖...")
                                    self._clean_node_modules()
                                    self.install_dependencies()
                        return False, f"项目重新构建失败，已尝试{attempt + 1}次: {stderr_str}"
                    else:
                        return False, f"重新安装依赖失败: {install_msg}"
                return False, f"项目构建失败: {stderr_str}"
            
            return True, "项目构建成功"
        except subprocess.TimeoutExpired:
            return False, "项目构建超时"
        except Exception as e:
            return False, f"构建项目时发生错误: {str(e)}"

    def build_project_to_target(self, target_path: str) -> Tuple[bool, str]:
        """构建项目并移动到目标路径"""
        # 直接使用传统的构建后移动方式
        success, message = self.build_project()
        if success:
            success_move, message_move = self.move_build_output(target_path)
            return success_move, message_move
        else:
            return False, message
    
    def check_dist_folder(self) -> Tuple[bool, str]:
        """检查构建输出目录"""
        return self.strategy.get_build_output_path(self.project_path)
    
    def move_build_output(self, target_path: str) -> Tuple[bool, str]:
        """移动构建结果到目标路径"""
        success, result = self.check_dist_folder()
        if not success:
            return False, result
        
        build_output_path = result
        
        try:
            # 检查源目录是否为空
            if not os.path.exists(build_output_path) or not os.listdir(build_output_path):
                return False, f"构建输出目录为空或不存在: {build_output_path}"
            
            # 创建目标路径（如果不存在）
            os.makedirs(target_path, exist_ok=True)
            
            # 清空目标路径中的内容
            if os.path.exists(target_path):
                for filename in os.listdir(target_path):
                    file_path = os.path.join(target_path, filename)
                    if os.path.isfile(file_path) or os.path.islink(file_path):
                        os.unlink(file_path)
                    elif os.path.isdir(file_path):
                        shutil.rmtree(file_path)
            
            # 移动构建结果到目标路径
            moved_files = 0
            for filename in os.listdir(build_output_path):
                src_path = os.path.join(build_output_path, filename)
                dst_path = os.path.join(target_path, filename)
                # 尝试移动文件/目录，如果在同一文件系统上会更高效
                try:
                    shutil.move(src_path, dst_path)
                    moved_files += 1
                except (OSError, shutil.Error) as e:
                    # 如果移动失败（例如跨文件系统或权限问题），则使用复制
                    try:
                        if os.path.isfile(src_path) or os.path.islink(src_path):
                            shutil.copy2(src_path, dst_path)
                            moved_files += 1
                        elif os.path.isdir(src_path):
                            shutil.copytree(src_path, dst_path)
                            moved_files += 1
                    except Exception as copy_error:
                        print(f"复制文件 {filename} 时出错: {copy_error}")
                except Exception as e:
                    print(f"移动文件 {filename} 时出错: {e}")
            
            return True, f"成功移动 {moved_files} 个文件/目录到: {target_path}"
        except Exception as e:
            return False, f"移动构建结果时发生错误: {str(e)}"